package esmska.gui;
import esmska.Context;
import esmska.data.Config;
import esmska.data.Contact;
import esmska.data.Contacts;
import esmska.data.CountryPrefix;
import esmska.data.Envelope;
import esmska.data.Keyring;
import esmska.data.Links;
import esmska.data.Gateway;
import esmska.data.Gateway.Feature;
import esmska.data.Gateways;
import esmska.data.Icons;
import esmska.data.Queue;
import esmska.data.SMS;
import esmska.data.event.AbstractDocumentListener;
import esmska.data.event.ActionEventSupport;
import esmska.utils.L10N;
import esmska.utils.MiscUtils;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import javax.swing.GroupLayout.Alignment;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import javax.swing.text.Element;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import javax.swing.text.StyledDocument;
import javax.swing.undo.UndoManager;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.openide.awt.Mnemonics;
import org.pushingpixels.substance.api.SubstanceLookAndFeel;
import org.pushingpixels.substance.api.skin.SkinChangeListener;
/** Panel for writing and sending sms, and for setting immediate contact
*
* @author ripper
*/
public class SMSPanel extends javax.swing.JPanel {
private static final Logger logger = Logger.getLogger(SMSPanel.class.getName());
private static final ResourceBundle l10n = L10N.l10nBundle;
/** box for messages */
private Envelope envelope = new Envelope();
/** support for undo and redo in sms text pane */
private UndoManager smsTextUndoManager = new UndoManager();
private SortedSet<Contact> contacts = Contacts.getInstance().getAll();
private Config config = Config.getInstance();
private Keyring keyring = Keyring.getInstance();
private UndoAction undoAction = new UndoAction();
private RedoAction redoAction = new RedoAction();
private Action showAddContactDialogAction = new ShowAddContactDialogAction();
private CompressAction compressAction = null;
private SendAction sendAction = new SendAction();
private SMSTextPaneListener smsTextPaneListener = new SMSTextPaneListener();
private SMSTextPaneDocumentFilter smsTextPaneDocumentFilter;
private RecipientTextField recipientField;
private boolean disableContactListeners;
// <editor-fold defaultstate="collapsed" desc="ActionEvent support">
private ActionEventSupport actionSupport = new ActionEventSupport(this);
public void addActionListener(ActionListener actionListener) {
actionSupport.addActionListener(actionListener);
}
public void removeActionListener(ActionListener actionListener) {
actionSupport.removeActionListener(actionListener);
}
// </editor-fold>
/** Creates new form SMSPanel */
public SMSPanel() {
initComponents();
compressAction = new CompressAction();
recipientField = (RecipientTextField) recipientTextField;
//if not Substance LaF, add clipboard popup menu to text components
if (!config.getLookAndFeel().equals(ThemeManager.LAF.SUBSTANCE)) {
ClipboardPopupMenu.register(smsTextPane);
ClipboardPopupMenu.register(recipientTextField);
}
// on keyring update update credentialsInfoLabel
keyring.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateCredentialsInfoLabel();
SMSPanel.this.revalidate();
}
});
// allow to send messages once the program is fully loaded
Context.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (!StringUtils.equals(evt.getPropertyName(), "everythingLoaded")) {
return;
}
sendAction.updateStatus();
}
});
}
/** validates sms form and returns status */
private boolean validateForm(boolean transferFocus) {
if (StringUtils.isEmpty(envelope.getText())) {
if (transferFocus) {
smsTextPane.requestFocusInWindow();
}
return false;
}
if (envelope.getText().length() > envelope.getMaxTextLength()) {
if (transferFocus) {
smsTextPane.requestFocusInWindow();
}
return false;
}
if (envelope.getContacts().size() <= 0) {
if (transferFocus) {
recipientTextField.requestFocusInWindow();
}
return false;
}
for (Contact c : envelope.getContacts()) {
if (!Contact.isValidNumber(c.getNumber())) {
if (transferFocus) {
recipientTextField.requestFocusInWindow();
}
return false;
}
}
return true;
}
/** Find contact according to filled name/number and gateway
* @param onlyFullMatch whether to look only for full match (name/number and gateway)
* or even partial match (name/number only)
* @return found contact or null if none found
*/
private Contact lookupContact(boolean onlyFullMatch) {
String number = recipientField.getNumber();
String id = recipientTextField.getText(); //name or number
String gatewayName = gatewayComboBox.getSelectedGatewayName();
if (StringUtils.isEmpty(id)) {
return null;
}
Contact contact = null; //match on id
Contact fullContact = null; //match on id and gateway
//search in contact numbers
if (number != null) {
for (Contact c : contacts) {
if (ObjectUtils.equals(c.getNumber(), number)) {
if (ObjectUtils.equals(c.getGateway(), gatewayName)) {
fullContact = c;
break;
}
if (!onlyFullMatch && contact == null) {
contact = c; //remember only first partial match, but search further
}
}
}
} else {
//search in contact names if not number
for (Contact c : contacts) {
if (id.equalsIgnoreCase(c.getName())) {
if (ObjectUtils.equals(c.getGateway(), gatewayName)) {
fullContact = c;
break;
}
if (!onlyFullMatch && contact == null) {
contact = c; //remember only first partial match, but search further
}
}
}
}
return (fullContact != null ? fullContact : contact);
}
/** Request a contact to be selected in contact list. Use null for clearing
* the selection.
*/
private void requestSelectContact(Contact contact) {
if (contact != null) {
Context.mainFrame.getContactPanel().setSelectedContact(contact);
} else {
Context.mainFrame.getContactPanel().clearSelection();
}
}
/** set selected contacts in contact list or contact to display */
public void setContacts(Collection<Contact> contacts) {
Validate.notNull(contacts);
disableContactListeners = true;
stripSignature();
int count = contacts.size();
if (count == 1) {
Contact c = contacts.iterator().next();
recipientField.setContact(c);
gatewayComboBox.setSelectedGateway(c.getGateway());
}
boolean multiSendMode = (count > 1);
if (multiSendMode) {
recipientTextField.setText(l10n.getString("Multiple_sending"));
gatewayComboBox.setSelectedGateway(null);
}
recipientTextField.setEnabled(! multiSendMode);
gatewayComboBox.setEnabled(! multiSendMode);
//update envelope
Set<Contact> set = new HashSet<Contact>();
set.addAll(contacts);
if (count < 1) {
Contact contact = recipientField.getContact();
set.add(new Contact(contact != null ? contact.getName() : null,
recipientField.getNumber(),
gatewayComboBox.getSelectedGatewayName()));
}
envelope.setContacts(set);
// update components
updateSignature();
sendAction.updateStatus();
smsTextPaneDocumentFilter.requestUpdate();
updateNumberInfoLabel();
updateSuggestGatewayButton();
updateAddContactButton();
revalidate();
disableContactListeners = false;
}
/** set sms to display and edit */
public void setSMS(final SMS sms) {
recipientField.setNumber(sms.getNumber());
smsTextPane.setText(sms.getText());
smsTextUndoManager.discardAllEdits();
//recipient textfield will change gateway, must wait and change gateway back
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
gatewayComboBox.setSelectedGateway(sms.getGateway());
smsTextPane.requestFocusInWindow();
}
});
}
/** get currently written sms text
* @return currently written sms text or empty string; never null
*/
public String getText() {
String text = smsTextPane.getText();
return text != null ? text : "";
}
/** get undo action used in sms text pane */
public Action getUndoAction() {
return undoAction;
}
/** get redo action used in sms text pane */
public Action getRedoAction() {
return redoAction;
}
/** get compress action used for compressing sms text */
public Action getCompressAction() {
return compressAction;
}
/** get send action used for sending the sms */
public Action getSendAction() {
return sendAction;
}
/* Remove current sender name from message text */
private void stripSignature() {
String signatureName = envelope.getSenderName();
if (StringUtils.isEmpty(signatureName)) {
return;
}
String text = smsTextPane.getText();
if (text.startsWith(signatureName)) {
smsTextPane.setText(StringUtils.removeStart(text, signatureName));
}
}
/* Add sender name to the message text if appropriate */
private void updateSignature() {
String signatureName = envelope.getSenderName();
if (StringUtils.isEmpty(signatureName)) {
return;
}
String text = smsTextPane.getText();
if (text.startsWith(signatureName)) {
return;
}
smsTextPane.setText(signatureName + text);
}
/** updates values on progress bars according to currently written message chars*/
private void updateProgressBars() {
int currentLength = envelope.getText().length();
int smsLength = envelope.getSMSLength();
int maxTextLength = envelope.getMaxTextLength();
//set limits
fullProgressBar.setMaximum(maxTextLength);
int min = envelope.getPenultimateIndexOfCut(envelope.getText(), smsLength);
int max = min + smsLength;
max = Math.min(max, maxTextLength);
singleProgressBar.setMinimum(min);
singleProgressBar.setMaximum(max);
//set values
fullProgressBar.setValue(currentLength);
singleProgressBar.setValue(currentLength);
//set tooltips
updateProgressBarToolTip(fullProgressBar, "SMSPanel.fullProgressBar");
updateProgressBarToolTip(singleProgressBar, "SMSPanel.singleProgressBar");
}
/** Updates the tooltip for the progress bar according to its current value and limits
* @param bar The progress bar
* @param resourceId The localization string name
*/
private void updateProgressBarToolTip(JProgressBar bar, String resourceId)
{
int used = bar.getValue() - bar.getMinimum();
if (bar.getMaximum() == Integer.MAX_VALUE) {
//don't show really big numbers when there is no limit on characters
bar.setToolTipText(MessageFormat.format(l10n.getString(resourceId),
used, "∞", "∞"));
} else {
int capacity = bar.getMaximum() - bar.getMinimum();
int remaining = bar.getMaximum() - bar.getValue();
bar.setToolTipText(MessageFormat.format(l10n.getString(resourceId),
used, capacity, remaining));
}
}
/** Show warning if user selected gateway that does not send
* messages to numbers with specified prefix
*/
private void updateCountryInfoLabel() {
countryInfoLabel.setVisible(false);
//ensure that fields are sufficiently filled in
Gateway gateway = gatewayComboBox.getSelectedGateway();
String number = recipientField.getNumber();
if (gateway == null || !Contact.isValidNumber(number)) {
return;
}
boolean supported = Gateways.isNumberSupported(gateway, number);
if (!supported) {
String text = MessageFormat.format(l10n.getString("SMSPanel.countryInfoLabel.text"),
StringUtils.join(gateway.getSupportedPrefixes(), ','));
countryInfoLabel.setText(text);
countryInfoLabel.setVisible(true);
}
}
/** Show warning if user selected gateway requiring registration
* and no credentials are filled in
*/
private void updateCredentialsInfoLabel() {
Gateway gateway = gatewayComboBox.getSelectedGateway();
if (gateway != null && gateway.hasFeature(Feature.LOGIN_ONLY) &&
keyring.getKey(gateway.getName()) == null) {
credentialsInfoLabel.setVisible(true);
} else {
credentialsInfoLabel.setVisible(false);
}
}
/** Show warning if the recipient number is not in a valid international
* format.
*/
private void updateNumberInfoLabel() {
numberInfoLabel.setVisible(false);
if (envelope.getContacts().size() != 1) {
//multisend mode or no contact selected
return;
}
Contact contact = envelope.getContacts().iterator().next();
if (StringUtils.isNotEmpty(recipientField.getText())) {
numberInfoLabel.setVisible(!Contact.isValidNumber(contact.getNumber()));
}
}
/** Show or hide addContactButton, depending whether recipientTextField contains an unknown number */
private void updateAddContactButton() {
RecipientTextField field = (RecipientTextField) recipientTextField;
if (field.getContact() == null && field.getNumber() != null) {
addContactButton.setVisible(true);
} else {
addContactButton.setVisible(false);
}
}
/** Show or hide suggest button */
private void updateSuggestGatewayButton() {
RecipientTextField field = (RecipientTextField) recipientTextField;
ArrayList<Gateway> gws = new ArrayList<Gateway>();
if (field.getContact() == null && field.getNumber() != null) {
gws = Gateways.getInstance().suggestGateway(field.getNumber()).get1();
}
boolean visible = false;
if (gws.size() > 1) {
visible = true;
}
if (gws.size() == 1 && gatewayComboBox.getSelectedGateway() != gws.get(0)) {
visible = true;
}
suggestGatewayButton.setVisible(visible);
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
gatewayComboBox = new GatewayComboBox();
fullProgressBar = new JProgressBar();
jScrollPane1 = new JScrollPane();
smsTextPane = new JTextPane();
textLabel = new JLabel();
sendButton = new JButton();
smsCounterLabel = new JLabel();
singleProgressBar = new JProgressBar();
gatewayLabel = new JLabel();
recipientTextField = new SMSPanel.RecipientTextField();
recipientLabel = new JLabel();
infoPanel = new JPanel();
credentialsInfoLabel = new InfoLabel();
numberInfoLabel = new InfoLabel();
countryInfoLabel = new InfoLabel();
suggestGatewayButton = new JButton();
jLabel1 = new JLabel();
addContactButton = new JButton();
setBorder(BorderFactory.createTitledBorder(l10n.getString("SMSPanel.border.title"))); // NOI18N
setMinimumSize(new Dimension(5, 5));
addFocusListener(new FocusAdapter() {
public void focusGained(FocusEvent evt) {
formFocusGained(evt);
}
});
gatewayComboBox.addActionListener(new GatewayComboBoxActionListener());
gatewayComboBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent evt) {
gatewayComboBoxItemStateChanged(evt);
}
});
fullProgressBar.setMaximum(Gateway.maxMessageLength);
smsTextPane.getDocument().addDocumentListener(smsTextPaneListener);
smsTextPaneDocumentFilter = new SMSTextPaneDocumentFilter();
((AbstractDocument)smsTextPane.getStyledDocument()).setDocumentFilter(smsTextPaneDocumentFilter);
//bind actions and listeners
smsTextUndoManager.setLimit(-1);
smsTextPane.getDocument().addUndoableEditListener(new UndoableEditListener() {
public void undoableEditHappened(UndoableEditEvent e) {
if (e.getEdit().getPresentationName().contains("style"))
return;
smsTextUndoManager.addEdit(e.getEdit());
}
});
int menuMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
String command = "undo";
smsTextPane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z, menuMask), command);
smsTextPane.getActionMap().put(command,undoAction);
command = "redo";
smsTextPane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Y, menuMask), command);
smsTextPane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
menuMask|KeyEvent.SHIFT_DOWN_MASK), command);
smsTextPane.getActionMap().put(command, redoAction);
//ctrl+enter
command = "send";
smsTextPane.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, menuMask), command);
smsTextPane.getActionMap().put(command, sendAction);
jScrollPane1.setViewportView(smsTextPane);
textLabel.setLabelFor(smsTextPane);
Mnemonics.setLocalizedText(textLabel, l10n.getString("SMSPanel.textLabel.text")); // NOI18N
textLabel.setToolTipText(l10n.getString("SMSPanel.textLabel.toolTipText")); // NOI18N
sendButton.setAction(sendAction);
sendButton.setToolTipText(l10n.getString("SMSPanel.sendButton.toolTipText")); // NOI18N
Mnemonics.setLocalizedText(smsCounterLabel, l10n.getString("SMSPanel.smsCounterLabel.text")); // NOI18N
singleProgressBar.setMaximum(Gateway.maxMessageLength);
gatewayLabel.setLabelFor(gatewayComboBox);
Mnemonics.setLocalizedText(gatewayLabel, l10n.getString("SMSPanel.gatewayLabel.text")); // NOI18N
gatewayLabel.setToolTipText(gatewayComboBox.getToolTipText());
recipientLabel.setLabelFor(recipientTextField);
Mnemonics.setLocalizedText(recipientLabel, l10n.getString("SMSPanel.recipientLabel.text")); // NOI18N
recipientLabel.setToolTipText(recipientTextField.getToolTipText());
infoPanel.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent evt) {
infoPanelComponentResized(evt);
}
});
Mnemonics.setLocalizedText(credentialsInfoLabel, l10n.getString("SMSPanel.credentialsInfoLabel.text")); // NOI18N
credentialsInfoLabel.setText(MessageFormat.format(
l10n.getString("SMSPanel.credentialsInfoLabel.text"), Links.CONFIG_GATEWAYS));
credentialsInfoLabel.setVisible(false);
Mnemonics.setLocalizedText(numberInfoLabel, l10n.getString("SMSPanel.numberInfoLabel.text")); // NOI18N
numberInfoLabel.setVisible(false);
Mnemonics.setLocalizedText(countryInfoLabel, l10n.getString("SMSPanel.countryInfoLabel.text")); // NOI18N
countryInfoLabel.setVisible(false);
GroupLayout infoPanelLayout = new GroupLayout(infoPanel);
infoPanel.setLayout(infoPanelLayout);
infoPanelLayout.setHorizontalGroup(
infoPanelLayout.createParallelGroup(Alignment.LEADING)
.addComponent(credentialsInfoLabel, GroupLayout.DEFAULT_SIZE, 368, Short.MAX_VALUE)
.addComponent(numberInfoLabel)
.addComponent(countryInfoLabel, GroupLayout.DEFAULT_SIZE, 368, Short.MAX_VALUE)
);
infoPanelLayout.setVerticalGroup(
infoPanelLayout.createParallelGroup(Alignment.LEADING)
.addGroup(Alignment.TRAILING, infoPanelLayout.createSequentialGroup()
.addComponent(numberInfoLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(countryInfoLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(credentialsInfoLabel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
);
suggestGatewayButton.setAction(Actions.getSuggestGatewayAction(gatewayComboBox, recipientTextField));
suggestGatewayButton.setIcon(new ImageIcon(getClass().getResource("/esmska/resources/search-16.png"))); // NOI18N
suggestGatewayButton.setText(null);
suggestGatewayButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE);
addContactButton.setAction(showAddContactDialogAction);
addContactButton.setIcon(new ImageIcon(getClass().getResource("/esmska/resources/add-16.png"))); // NOI18N
addContactButton.setText(null);
addContactButton.putClientProperty(SubstanceLookAndFeel.FLAT_PROPERTY, Boolean.TRUE);
GroupLayout layout = new GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(Alignment.LEADING)
.addGroup(Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(recipientLabel)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(recipientTextField, GroupLayout.DEFAULT_SIZE, 325, Short.MAX_VALUE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(addContactButton))
.addGroup(layout.createSequentialGroup()
.addComponent(gatewayLabel)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(gatewayComboBox, GroupLayout.DEFAULT_SIZE, 325, Short.MAX_VALUE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(suggestGatewayButton)
.addGap(0, 0, 0)
.addComponent(jLabel1))
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(Alignment.LEADING)
.addComponent(textLabel)
.addComponent(singleProgressBar, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE)
.addComponent(fullProgressBar, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE))
.addPreferredGap(ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(Alignment.TRAILING)
.addGroup(layout.createSequentialGroup()
.addComponent(smsCounterLabel, GroupLayout.DEFAULT_SIZE, 341, Short.MAX_VALUE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(sendButton))
.addComponent(jScrollPane1, GroupLayout.DEFAULT_SIZE, 378, Short.MAX_VALUE)))))
.addGroup(Alignment.TRAILING, layout.createSequentialGroup()
.addGap(74, 74, 74)
.addComponent(infoPanel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
.addGap(10, 10, 10))
);
layout.linkSize(SwingConstants.HORIZONTAL, new Component[] {fullProgressBar, singleProgressBar});
layout.linkSize(SwingConstants.HORIZONTAL, new Component[] {gatewayLabel, recipientLabel, textLabel});
layout.setVerticalGroup(
layout.createParallelGroup(Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(Alignment.BASELINE)
.addComponent(recipientLabel)
.addComponent(recipientTextField, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addComponent(addContactButton))
.addPreferredGap(ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(Alignment.LEADING)
.addGroup(layout.createParallelGroup(Alignment.TRAILING)
.addGroup(layout.createParallelGroup(Alignment.BASELINE)
.addComponent(gatewayLabel)
.addComponent(gatewayComboBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))
.addComponent(suggestGatewayButton))
.addComponent(jLabel1, GroupLayout.PREFERRED_SIZE, 28, GroupLayout.PREFERRED_SIZE))
.addPreferredGap(ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(Alignment.LEADING)
.addGroup(Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(jScrollPane1, GroupLayout.DEFAULT_SIZE, 65, Short.MAX_VALUE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(infoPanel, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(Alignment.LEADING, false)
.addComponent(sendButton)
.addComponent(smsCounterLabel)))
.addGroup(layout.createSequentialGroup()
.addComponent(textLabel)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(singleProgressBar, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE)
.addPreferredGap(ComponentPlacement.RELATED)
.addComponent(fullProgressBar, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE)))
.addContainerGap())
);
layout.linkSize(SwingConstants.VERTICAL, new Component[] {fullProgressBar, singleProgressBar});
layout.linkSize(SwingConstants.VERTICAL, new Component[] {addContactButton, recipientTextField});
layout.linkSize(SwingConstants.VERTICAL, new Component[] {gatewayComboBox, suggestGatewayButton});
}// </editor-fold>//GEN-END:initComponents
private void formFocusGained(FocusEvent evt) {//GEN-FIRST:event_formFocusGained
smsTextPane.requestFocusInWindow();
}//GEN-LAST:event_formFocusGained
private void gatewayComboBoxItemStateChanged(ItemEvent evt) {//GEN-FIRST:event_gatewayComboBoxItemStateChanged
updateCredentialsInfoLabel();
updateCountryInfoLabel();
updateSuggestGatewayButton();
revalidate();
}//GEN-LAST:event_gatewayComboBoxItemStateChanged
private void infoPanelComponentResized(ComponentEvent evt) {//GEN-FIRST:event_infoPanelComponentResized
if (MiscUtils.needsResize(this, MiscUtils.Direction.HEIGHT)) {
actionSupport.fireActionPerformed(ActionEventSupport.ACTION_NEED_RESIZE, null);
}
}//GEN-LAST:event_infoPanelComponentResized
/** Send sms to queue */
private class SendAction extends AbstractAction {
public SendAction() {
L10N.setLocalizedText(this, l10n.getString("Send_"));
putValue(SMALL_ICON, Icons.get("send-16.png"));
putValue(LARGE_ICON_KEY, Icons.get("send-22.png"));
putValue(SHORT_DESCRIPTION,l10n.getString("Send_message"));
putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
setEnabled(false);
}
@Override
public void actionPerformed(ActionEvent e) {
if (!validateForm(true)) {
return;
}
logger.fine("Sending new message to queue");
Queue.getInstance().addAll(envelope.generate());
smsTextPane.setText(null);
smsTextUndoManager.discardAllEdits();
smsTextPane.requestFocusInWindow();
}
/** update status according to current conditions */
public void updateStatus() {
this.setEnabled(validateForm(false) && Context.everythingLoaded());
}
}
/** undo in sms text pane */
private class UndoAction extends AbstractAction {
public UndoAction() {
L10N.setLocalizedText(this, l10n.getString("Undo_"));
putValue(SMALL_ICON, Icons.get("undo-16.png"));
putValue(LARGE_ICON_KEY, Icons.get("undo-32.png"));
putValue(SHORT_DESCRIPTION, l10n.getString("SMSPanel.Undo_change_in_message_text"));
putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Z,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
}
@Override
public void actionPerformed(ActionEvent e) {
if (smsTextUndoManager.canUndo()) {
smsTextUndoManager.undo();
smsTextPaneDocumentFilter.requestUpdate();
}
}
/** update status according to current conditions */
public void updateStatus() {
setEnabled(smsTextUndoManager.canUndo());
}
}
/** redo in sms text pane */
private class RedoAction extends AbstractAction {
public RedoAction() {
L10N.setLocalizedText(this, l10n.getString("Redo_"));
putValue(SMALL_ICON, Icons.get("redo-16.png"));
putValue(LARGE_ICON_KEY, Icons.get("redo-32.png"));
putValue(SHORT_DESCRIPTION, l10n.getString("SMSPanel.Redo_change_in_message_text"));
putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Y,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
}
@Override
public void actionPerformed(ActionEvent e) {
if (smsTextUndoManager.canRedo()) {
smsTextUndoManager.redo();
smsTextPaneDocumentFilter.requestUpdate();
}
}
/** update status according to current conditions */
public void updateStatus() {
setEnabled(smsTextUndoManager.canRedo());
}
}
/** compress current sms text by rewriting it to CamelCase */
private class CompressAction extends AbstractAction {
/** is message selected just partially or as a whole? */
private boolean partialSelection = false;
public CompressAction() {
updateLabels();
putValue(SHORT_DESCRIPTION,l10n.getString("SMSPanel.compress"));
putValue(LARGE_ICON_KEY, Icons.get("compress-32.png"));
putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_K,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
// add listener for selection changes and update labels accordingly
smsTextPane.addCaretListener(new CaretListener() {
@Override
public void caretUpdate(CaretEvent e) {
boolean ps = (smsTextPane.getSelectedText() != null);
if (ps != partialSelection) {
partialSelection = ps;
updateLabels();
}
}
});
}
/** updates labels according to current status */
private void updateLabels() {
if (partialSelection) {
L10N.setLocalizedText(this, l10n.getString("CompressText_"));
} else {
L10N.setLocalizedText(this, l10n.getString("Compress_"));
}
}
@Override
public void actionPerformed(ActionEvent e) {
logger.fine("Compressing message");
String text = partialSelection ? smsTextPane.getSelectedText() : smsTextPane.getText();
if (StringUtils.isEmpty(text)) {
return;
}
String newText = text.replaceAll("\\s", " "); //all whitespace to spaces
newText = Pattern.compile("(\\s)\\s+", Pattern.DOTALL).matcher(newText).replaceAll("$1"); //remove duplicate whitespaces
Pattern pattern = Pattern.compile("\\s+(.)", Pattern.DOTALL);
Matcher matcher = pattern.matcher(newText);
while (matcher.find()) { //find next space+character
newText = matcher.replaceFirst(matcher.group(1).toUpperCase()); //replace by upper character
matcher = pattern.matcher(newText);
}
newText = newText.replaceAll(" $", ""); //remove trailing space
if (newText.equals(text)) { //do not replace if already compressed
return;
}
// replace the old text with the compressed one
if (partialSelection) {
// replace text and leave it selected
int selectIndex = smsTextPane.getSelectionStart();
smsTextPane.replaceSelection(newText);
smsTextPane.setSelectionStart(selectIndex);
smsTextPane.setSelectionEnd(selectIndex + newText.length());
} else {
smsTextPane.setText(newText);
}
}
/** update status according to current conditions */
public void updateStatus() {
setEnabled(getText().length() > 0);
}
}
/** Show a dialog to add a new contact with unknown number from recipientTextField */
private class ShowAddContactDialogAction extends AbstractAction {
private Contact skeleton;
public ShowAddContactDialogAction() {
this.putValue(SHORT_DESCRIPTION, l10n.getString("Add_contact"));
}
@Override
public void actionPerformed(ActionEvent e) {
skeleton = new Contact(null, recipientTextField.getText(), null);
Context.mainFrame.getContactPanel().showAddContactDialog(skeleton);
}
}
/** Another gateway selected */
private class GatewayComboBoxActionListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (disableContactListeners) {
return;
}
stripSignature();
//select contact only if full match found
Contact contact = lookupContact(true);
if (contact != null) {
requestSelectContact(contact);
}
//update envelope
Set<Contact> set = new HashSet<Contact>();
Contact c = recipientField.getContact();
set.add(new Contact(c != null ? c.getName() : null,
recipientField.getNumber(),
gatewayComboBox.getSelectedGatewayName()));
envelope.setContacts(set);
//update text editor listeners
DocumentEvent event = new DocumentEvent() {
@Override
public DocumentEvent.ElementChange getChange(Element elem) {
return null;
}
@Override
public Document getDocument() {
return smsTextPane.getDocument();
}
@Override
public int getLength() {
return 0;
}
@Override
public int getOffset() {
return 0;
}
@Override
public DocumentEvent.EventType getType() {
return DocumentEvent.EventType.INSERT;
}
};
smsTextPaneListener.onUpdate(event);
//update components
updateSignature();
smsTextPaneDocumentFilter.requestUpdate();
}
}
/** Listener for sms text pane */
private class SMSTextPaneListener extends AbstractDocumentListener {
/** count number of chars in sms and take action */
private void countChars(DocumentEvent e) {
String msgText = envelope.getText();
if (envelope.getSMSLength() > 0) {
int smsCount = envelope.getSMSCount(msgText, envelope.getSMSLength()); //num of sms
smsCounterLabel.setText(MessageFormat.format(
l10n.getString("SMSPanel.smsCounterLabel.1"),
msgText.length(), smsCount));
} else {
smsCounterLabel.setText(MessageFormat.format(
l10n.getString("SMSPanel.smsCounterLabel.1"),
msgText.length(), "?"));
}
if (msgText.length() > envelope.getMaxTextLength()) {
//chars more than max
smsCounterLabel.setForeground(Color.RED);
smsCounterLabel.setText(MessageFormat.format(l10n.getString("SMSPanel.smsCounterLabel.2"),
msgText.length()));
} else {
//chars ok
smsCounterLabel.setForeground(UIManager.getColor("Label.foreground"));
}
}
/** update envelope with the new text */
private void updateEnvelope(DocumentEvent e) {
try {
envelope.setText(e.getDocument().getText(0,e.getDocument().getLength()));
} catch (BadLocationException ex) {
logger.log(Level.SEVERE, "Error getting sms text", ex);
}
}
@Override
public void onUpdate(DocumentEvent e) {
updateEnvelope(e);
countChars(e);
}
}
/** Limit maximum sms length and color it */
private class SMSTextPaneDocumentFilter extends DocumentFilter {
private StyledDocument doc;
private Style regular, highlight;
private Style lastStyle = regular;
private Color alternateTextColor = Color.BLUE;
//updating after each event is slow, therefore there is timer
private Timer timer = new Timer(100, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
colorDocument();
updateUI();
}
});
public SMSTextPaneDocumentFilter() {
super();
timer.setRepeats(false);
//set styles
doc = smsTextPane.getStyledDocument();
Style def = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
regular = doc.addStyle("regular", def);
highlight = doc.addStyle("highlight", def);
lafChangedImpl();
// listen for changes in Look and Feel and change color of regular text
UIManager.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ("lookAndFeel".equals(evt.getPropertyName())) {
lafChanged();
}
}
});
// the same for substance skins (they do not notify UIManager from some reason)
SubstanceLookAndFeel.registerSkinChangeListener(new SkinChangeListener() {
@Override
public void skinChanged() {
lafChanged();
}
});
}
/** update components and actions */
private void updateUI() {
compressAction.updateStatus();
undoAction.updateStatus();
redoAction.updateStatus();
sendAction.updateStatus();
updateProgressBars();
}
/** color parts of sms */
private void colorDocument() {
String msgText = envelope.getText();
ArrayList<Integer> cutIndexes = envelope.getIndicesOfCuts(msgText,
envelope.getSMSLength());
// if empty cutIndexes, the whole text should be black
if (cutIndexes.isEmpty()) {
cutIndexes.add(msgText.length());
}
int from = 0;
for (int i = 0; i < cutIndexes.size(); i++) {
int to = cutIndexes.get(i);
lastStyle = getStyle(i+1);
doc.setCharacterAttributes(from, to - from, lastStyle, false);
from = to;
}
}
/** calculate which style is appropriate for given position */
private Style getStyle(int smsNum) {
if ((smsNum % 2) == 1) {
//even message
return regular;
} else {
//odd message
return highlight;
}
}
@Override
public void replace(DocumentFilter.FilterBypass fb, int offset, int length,
String text, AttributeSet attrs) throws BadLocationException {
//if reached size limit, crop the text and show a warning
String currentText=fb.getDocument().getText(0, fb.getDocument().getLength());
if ((currentText.length() + (text != null ? text.length() : 0) - length)
> envelope.getMaxTextLength()) {
Context.mainFrame.getStatusPanel().setStatusMessage(
l10n.getString("SMSPanel.Text_is_too_long!"), null, null, false);
Context.mainFrame.getStatusPanel().hideStatusMessageAfter(5000);
int maxlength = envelope.getMaxTextLength(currentText)
- currentText.length() + length;
maxlength = Math.max(maxlength, 0);
if (text != null) {
text = text.substring(0, maxlength);
}
}
super.replace(fb, offset, length, text, lastStyle);
timer.restart();
}
@Override
public void insertString(DocumentFilter.FilterBypass fb, int offset,
String string, AttributeSet attr) throws BadLocationException {
super.insertString(fb, offset, string, attr);
timer.restart();
}
@Override
public void remove(DocumentFilter.FilterBypass fb, int offset, int length)
throws BadLocationException {
super.remove(fb, offset, length);
timer.restart();
}
/** request recoloring externally */
public void requestUpdate() {
timer.restart();
}
/** update text color on LaF change */
private void lafChanged() {
lafChangedImpl();
SMSTextPaneDocumentFilter.this.requestUpdate();
}
/** main executive code for lafChanged() without any notifications or callbacks */
private void lafChangedImpl() {
StyleConstants.setForeground(regular, UIManager.getColor("TextArea.foreground"));
alternateTextColor = ThemeManager.isCurrentSkinDark() ? Color.CYAN : Color.BLUE;
StyleConstants.setForeground(highlight, alternateTextColor);
}
}
/** Textfield for entering contact name or number */
public class RecipientTextField extends JTextField {
/** currently selected contact */
private Contact contact;
private RecipientDocumentChange recipientDocumentChange = new RecipientDocumentChange();
private String tooltip = l10n.getString("SMSPanel.recipientTextField.tooltip");
private String tooltipTip = l10n.getString("SMSPanel.recipientTextField.tooltip.tip");
public RecipientTextField() {
//set tooltip
if (StringUtils.isEmpty(config.getCountryPrefix())) {
setToolTipText(tooltip + tooltipTip + "</html>");
} else {
setToolTipText(tooltip + "</html>");
}
//focus listener
addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
selectAll();
}
@Override
public void focusLost(FocusEvent e) {
select(0, 0);
//try to rewrite phone number to contact name if possible
redrawContactName();
}
});
//key listener
addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent evt) {
//on Enter just focus the text area
if (evt != null && evt.getKeyCode() == KeyEvent.VK_ENTER) {
smsTextPane.requestFocusInWindow();
return;
}
}
});
//document listener
getDocument().addDocumentListener(new AbstractDocumentListener() {
@Override
public void onUpdate(DocumentEvent evt) {
if (disableContactListeners) {
return;
}
SwingUtilities.invokeLater(recipientDocumentChange);
}
});
}
/** Set contact to display. Will display contact name. Will not change
displayed text if user is currently editing it. */
public void setContact(Contact contact) {
this.contact = contact;
if (!hasFocus()) {
super.setText(contact != null ? contact.getName() : null);
}
}
/** Get currently chosen contact. May be null. */
public Contact getContact() {
return contact;
}
/** Return visible text. May be contact name or phone number (will include prefix).
May be null. */
@Override
public String getText() {
if (contact != null) {
return contact.getNumber();
}
String text = super.getText();
if (StringUtils.isNotEmpty(text) && !text.startsWith("+")) {
text = config.getCountryPrefix() + text;
}
//prepend country prefix if not present and text is a number
if (Contact.isValidNumber(text)) {
return text;
} else { //text is a name
return super.getText();
}
}
/** Set text to display. Will erase any internally remembered contact. */
@Override
public void setText(String text) {
contact = null;
super.setText(text);
}
/** Rewrite phone number to contact name. Used after user finished editing
the field. */
public void redrawContactName() {
if (contact == null) {
return;
}
boolean old = disableContactListeners;
disableContactListeners = true;
super.setText(contact.getName());
disableContactListeners = old;
}
/** Get phone number of chosen contact or typed phone number. May be null. */
public String getNumber() {
if (contact != null) {
return contact.getNumber();
}
String text = getText();
if (Contact.isValidNumber(text)) {
return text;
} else {
return null;
}
}
/** Set phone number to display. Handles country prefix correctly. */
public void setNumber(String number) {
if (StringUtils.isEmpty(number)) {
setText("");
}
setText(CountryPrefix.stripCountryPrefix(number,true));
}
/** Listener for changes in the recipient field */
private class RecipientDocumentChange implements Runnable {
@Override
public void run() {
//search for contact
contact = null;
contact = lookupContact(false);
requestSelectContact(null); //ensure contact selection will fire
requestSelectContact(contact); //event even if the same contact
//if not found and is number, guess gateway
if (contact == null && getNumber() != null) {
gatewayComboBox.selectSuggestedGateway(getNumber());
}
//update envelope
Set<Contact> set = new HashSet<Contact>();
set.add(new Contact(contact != null ? contact.getName() : null,
getNumber(), gatewayComboBox.getSelectedGatewayName()));
envelope.setContacts(set);
//update components
sendAction.updateStatus();
updateCountryInfoLabel();
updateNumberInfoLabel();
gatewayComboBox.setFilter(getNumber());
updateSuggestGatewayButton();
updateAddContactButton();
SMSPanel.this.revalidate();
}
}
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private JButton addContactButton;
private InfoLabel countryInfoLabel;
private InfoLabel credentialsInfoLabel;
private JProgressBar fullProgressBar;
private GatewayComboBox gatewayComboBox;
private JLabel gatewayLabel;
private JPanel infoPanel;
private JLabel jLabel1;
private JScrollPane jScrollPane1;
private InfoLabel numberInfoLabel;
private JLabel recipientLabel;
private JTextField recipientTextField;
private JButton sendButton;
private JProgressBar singleProgressBar;
private JLabel smsCounterLabel;
private JTextPane smsTextPane;
private JButton suggestGatewayButton;
private JLabel textLabel;
// End of variables declaration//GEN-END:variables
}